package cn.phecda.boot.tenant.core.service;

import cn.phecda.boot.common.lang.exception.BootRuntimeException;
import cn.phecda.boot.tenant.core.properties.DataSourceConfig;
import cn.phecda.boot.tenant.core.properties.TenantProperties;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.cache.RemovalListener;
import com.zaxxer.hikari.HikariDataSource;
import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.ObjectUtils;
import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
import org.springframework.util.Assert;

import javax.sql.DataSource;
import java.time.Duration;
import java.util.concurrent.ExecutionException;

/**
 * <p> 租户数据源服务，支持<code>HikariDataSource</code>数据源 </p>
 *
 * @author : wanpinwei
 */
@Slf4j
@RequiredArgsConstructor
public class TenantDataSourceService {
    private static final int MAX_IMUN_SIZE = 50;
    private static final Duration EXPIRE_AFTER_ACCESS = Duration.ofMinutes(120);
    private static final String TENANT_POOL_NAME_SUFFIX = "DataSource";

    private final DataSourceProperties dataSourceProperties;
    private final TenantProperties tenantProperties;

    private LoadingCache<String, DataSource> tenantDataSources;

    /**
     * 销毁。再应用程序关闭的时候调用
     */
    public void destory() {
        log.info("|Phecda Boot Tenant|- All tenant data sources will be CLOSED");
        tenantDataSources.cleanUp();
    }

    /**
     * 获取数据源
     *
     * @param tenantId 租户代码
     * @return
     */
    public DataSource getDataSource(String tenantId) {
        Assert.hasText(tenantId, "Argument [tenantId] must not be empty");

        if (log.isDebugEnabled()) {
            log.info("|Phecda Boot Tenant|- Get tenant [{}] datasource ", tenantId);
        }
        try {
            return tenantDataSources.get(tenantId);
        } catch (ExecutionException e) {
            throw new BootRuntimeException("Obtain datasource fail", e);
        }
    }

    /**
     * <p>创建数据源缓存， 使用懒模式，也就是在使用数据源时才会创建。</p>
     *
     * <p>根据在属性文件中定义的租户数据源信息，创建数据源，并放置在缓存中。</p>
     *
     * <p>缓存设置了过期时间，在属性<code>EXPIRE_AFTER_ACCESS</code>中定义，过期删除数据源，释放连接，
     * 使用时再创建</p>
     */
    @PostConstruct
    private void createCache() {
        tenantDataSources = CacheBuilder.newBuilder()
                .maximumSize(MAX_IMUN_SIZE)
                .expireAfterAccess(EXPIRE_AFTER_ACCESS)
                .removalListener((RemovalListener<String, DataSource>) removal -> {
                    HikariDataSource ds = (HikariDataSource) removal.getValue();
                    if (!ds.isClosed()) {
                        ds.close();
                    }

                    log.info("|Phecda Boot Tenant|- Tenant [{}] datasource [{}] is CLOSED", removal.getKey(), ds.getPoolName());
                })
                .build(new CacheLoader<String, DataSource>() {

                    @Override
                    public DataSource load(String tenantId) throws Exception {
                        DataSourceConfig config = tenantProperties.getDatasources().get(tenantId);
                        if (ObjectUtils.isEmpty(config)) {
                            throw new BootRuntimeException("Could NOT find tenant data source config: " + tenantId);
                        }
                        return createAndConfigureDataSource(tenantId, config);
                    }
                });
    }

    /**
     * 创建数据源，根据主数据源的属性创建
     *
     * @param tenantId 租户
     * @param config   配置信息
     * @return
     */
    private DataSource createAndConfigureDataSource(String tenantId, DataSourceConfig config) {
        HikariDataSource ds = dataSourceProperties.initializeDataSourceBuilder().type(HikariDataSource.class)
                .username(config.getUsername())
                .password(config.getPassword())
                .url(config.getUrl())
                .build();

        ds.setPoolName(tenantId + TENANT_POOL_NAME_SUFFIX);
        log.info("|Phecda Boot Tenant|- Creating tenant [{}] datasource [{}]", tenantId, ds.getPoolName());
        return ds;
    }

}
